How Does This Site Exist

[13 February 2025]




~This article explains the technical details of how I hosted this site in a frugal manner and overcame some technical challenges.~




Objectives



The objectives for this site was fairly simple.

1. Build this site in a very cost-effective way. This means saying no to
any sort of reccuring costs (excluding paying for the domain).
2. Raw markdown files can be uploaded as blogs without tinker around too much with the source code itself. (kind of like an ssg)
3. Site must be reliable(minimal down time)
4. Site must be super fast ⚡️




Problems & Solutions



Normally people would host their website using some kind of a VM on the cloud (AWS EC2 / GCP Compute Engine), but this, is of course a reccuring cost, and not to forget, the constant fear of waking up with a $7200 gcp bill overnight.



To tackle this, I decided to host my website on a Raspberry Pi 3B+ that I had lying around.



The next issue I had to tackle was not being able to port-forward on my router. If you din't already know, usually when people want to expose services running on their local network to the public internet, they do so by exposing a particular port.



I use both Airtel and Jio (load-balanced using a switch) as my ISP. Both of them don't allow you to port-forward. One way to get over this is to pay the ISP some extra money 🤑, but I want to run this as frugally as possible. Even if I were to port-forward, I would need to request for a static-IP, and the fact that ISP's use CGNATs to map IPs just complicates things even more.



To tackle this issue I turned to tunneling. This would essentially allow me to achieve the same end goal of exposing a service to the public internet. I used cloudflare tunnels for this website as it offers a host of features like encrypting data in transit (cloudflare can still see your data, but this does not bother me since I'm not sending around any sensitive info), and also DDOS protections and a bunch of other features that give me some peace of mind.



If I were to host my application without cloudflare tunneling I'd have to deal with SSL certificates, that again, you guessed it costs moneyy. While using tunnels, cloudflare manages the SSL certificates for me, free of cost.



Another important objective was to be able to write blogs/articles in markdown and then render and serve those as HTML pages along with some styling to make everything look presentable. I shall talk more about how I did this later.



Let’s Get Building !



Step 1 - Boot Ubuntu Server onto the RPI

Setting up your RPI



To create a bootable SD that has ubuntu server, you must first install the Raspberry Pi Imager. Once thats done, select ubuntu server as the operating system and flash that onto the SD card.
Tip: It's easier if you setup the root username and password before flashing the SD card, there is an option to do this in the RPI Imager Software.



Once thats done, plug in the SD card into the RPI and boot it up. You will be prompted to go through the setup installation process for ubuntu. Once that’s done, try ssh-ing into your RPI from your main PC / laptop.



Congrats! You just booted your RPI with Ubuntu server and ssh-ed into it.

Step 2 - Setting static IP and DHCP reservation

While using tunnels, you want to ensure that your RPI has a static local IP. In order to do this
go to your router and make a DHCP reservation and set a static IP corresponding to the mac address of your RPI. This step is slightly different for different routers, so I would suggest that you check, how it can be done on your router. Also make sure that you configure the RPI to use a static IP everytime.

Step 3 - Setting up the web-server

Make sure you have updated the system packages, and also log-in to git.


Quickly code up a flask web-app



First pip install flask. Tip: I use uv to manage my python packages and it’s versions, along with virtual environments, its super useful. UV is built with rust and package downloads are crazy fast compared to pip

from flask import Flask <br>
app = Flask(__name__) <br>
@app.route('/')
def hello_world():
    return render_template('index.html') <br>
if __name__ == '__main__':
    app.run(port = 6969)




Create a folder named templates in the same directory where the main.py is. This should contain the flask code is.



The directory structure should look like this:

project
|-> main.py
|-> templates
        |-> index.html



create a basic html file

<!DOCTYPE html>
<html>
<body> <br>
<h1>Welcome to my website</h1> <br>
</body>
</html>
``` <br>
<br><br>
Now once you run ```main.py``` you should be able to see the site locally on your pi. <br>
## Step 4 - WSGI and Production Mode
If you observed closely, while running the flask app, it would have displayed a message that says:
<br> <br>

WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
<br> <br> Okay, so let’s setup Gunicorn a popular WSGI. <br> Pip install **gunicorn**, then create a filed calledwsgi.py``` with the following contents:

from main import app <br>
if __name__ == "__main__":
    app.run()
``` <br>
<br>
 then in your terminal, do the following <br>
```bash
gunicorn --bind 0.0.0.0:6969 wsgi.app



What this essentially does is, launch the flask application on the specified port.


You should now be able to acess the following website from any device that is connected to the same local network that the pi is connected to.

ipaddr:6969

Replace the ipaddr with the actual ip address of the pi, eg 19.168.0.160:6969



Alright, now we know how to start and serve the gunicorn application, but we want gunicorn to start automatically everytime the system reboots and also recover in case of a crash. It should be able to take care of itself and serve 24/7 without our intervention.


To do this we create a systemd service, like so. In your terminal type:

sudo nano /etc/systemd/system/gunicorn-website.service

Note: you can use your own terminal-based editor of choice, I give used nano here, as its usually installed by default on most systems.


Then once your in the service file, type this

[Unit]
Description=Gunicorn instance to serve Flask webserver in personal-website
After=network.target <br>
[Service]
User=username
Group=www-data
WorkingDirectory=path to working directory of project
Environment="PATH= path to venv"
ExecStart=path to venv/bin/gunicorn --workers 2 --bind 0.0.0.0:4321 wsgi:app
Restart=always
RestartSec=5 <br>
[Install]
WantedBy=multi-user.target




Okay now let me break this down for you.


The Unit part gives a description and mentions when the service should start, in our case thats after the networking has started after booting up the pi.


Next, the username has to be replace with the actual username


www-data is the user that web servers on Ubuntu (Apache, nginx, for example) use by default for normal operation. So we can leave this as is


for working directory, execstart and environment, replace the path in accordance with your machine. They basically tell the program where to look for gunicorn and other packages in the virutal environment you created so it can start gunicorn.


finally, the restart, it tells the service to restart in case something happens. The 5 second delay is given as a buffer, so that it does not try to create many instances of the service at once.



Alright! We have sucessfully created a service, now lets test if its working and enable it.


Type in the following in the terminal:

sudo systemctl daemon-reload
sudo systemctl start gunicorn-website
sudo systemctl enable gunicorn-website
sudo systemctl status gunicorn-website



Once you are done with all this, if you see a 'active(running)' indicator, then you are all good to go, the service is up and running and is enabled.



Reverse Proxy Using NGINX



The last and final step is to create an Nginx reverse proxy for our web server. It is a good idea to have a reverse proxy while hosting a web server for a lot of reasons, which I shall not go over here, but do note that this is optional, and you can host the application without Nginx as well.



First let's install nginx. In the terminal type:

sudo apt install nginx
sudo nano /etc/nginx/sites-available/personal-website.conf
``` <br>
<br>
Then in the config file type: <br>

server {
listen 80;
server_name yourdomainname.com www.yourdomainname.com ;

location / {
include proxy_params;
proxy_pass http://0.0.0.0:6969;
}
}

<br>
When Nginx checks this config file, first it checks if you have visited the website from one of the given server_name. If so, it creates a proxy and lets you access the Gunicorn service running on port 6969.
<br>
Once that's done, we need to create a file link so anytime there is an update in the config file nginx can update automatically. To do that, type the following in the terminal: <br>
```bash
sudo ln -s /etc/nginx/sites-available/personal-website.conf /etc/nginx/sites-enabled/
sudo nginx -t
``` <br>
<br>
If you get a 'syntax is okay, test is sucessful' message that means you have configured nginx correctly! <br>
<br>
Now you can restart your nginx and make sure your firewall has given nginx permission and you are done! <br>
```bash
sudo systemctl restart nginx
sudo ufw allow "Nginx Full"

Note if you have a 502 bad gateway error, it is likely due to file permission error, give the user permissions to execute files





Now that we are done with creating the web-application, we need to connect it to our cloudflare tunnel in order for others outside our network to be able to see our website.


Checkout - Connecting Nginx Web-Server To Cloudflare Tunnels




Powered Not An SSG 😎